Colors {
    -- Keenan palette
    Colors.black = rgba(0.0, 0.0, 0.0, 1.0)

    Colors.darkpurple = rgba(0.549,0.565,0.757, 1.0)
    Colors.purple2 = rgba(0.106, 0.122, 0.54, 0.2)
    Colors.lightpurple = rgba(0.816,0.824, 0.902, 1.0)

    Colors.midnightblue = rgba(0.14, 0.16, 0.52, 1.0)
    Colors.lightslategray = rgba(0.50, 0.51, 0.69, 1.0)
    Colors.silver = rgba(0.71, 0.72, 0.79, 1.0)
    Colors.gainsboro = rgba(0.87, 0.87, 0.87, 1.0)

    Colors.white = rgba(1.0, 1.0, 1.0, 1.0)
    Colors.red = rgba(1.0, 0.0, 0.0, 1.0)
    Colors.pink = rgba(1.0, 0.4, 0.7, 1.0)
    Colors.yellow = rgba(1.0, 1.0, 0.0, 1.0)
    Colors.orange = rgba(1.0, 0.6, 0.0, 1.0)
    Colors.green = rgba(0.0, 1.0, 0.0, 1.0)
    Colors.blue = rgba(0.0, 0.0, 1.0, 1.0)
    Colors.sky = rgba(0.325, 0.718, 0.769, 1.0)
    Colors.none = rgba(0.0, 0.0, 0.0, 0.0)
}

G {
    G.repelWeight = 0.0
    G.repelWeight2 = 1.0 * G.repelWeight
    G.textMargin = 15.0
    G.stroke = 2.0
    G.spacing = 150.0
    G.scaleFactor = 100.0
    G.pi = 3.14159
    G.two_pi = 6.28319
    G.sqrt_22 = 1.414/2.0
    G.numArcPts = 20

    G.max_z = 0.0
    G.max_r = 0.65

    G.sphereRadius = 1.0
    G.dist_to_sphere = 2.0
    G.dist_to_sphere_center = G.dist_to_sphere + G.sphereRadius
    G.camera_z = G.dist_to_sphere + G.sphereRadius
    G.camera = [0.0, 0.0, -1.0 * G.camera_z]     -- x, y, z in math space 
    		  -- Note: Sphere has r=1.0, so the camera needs to be at sufficient distance from the sphere to be able to project on the near plane z=1
    G.dir = [0.0, 0.0, 1.0] -- looking down the +z-axis
    G.hfov = (-5.0, 5.0)
    G.vfov = (-5.0, 5.0)

    -- G.toScreen is calculated as such:
    -- imageSize (total width) = G.circle.r * 2.0
    --           = 0.353 * G.toScreen * 2.0
    -- Therefore, if imageSize = 400 (for the paper),
    -- then G.toScreen = 400 / (0.353 * 2) = 566.57

    G.toScreen = 566.5722
    G.imageSize = G.circle.r * G.toScreen * 2.0

    G.arrowheadSize = 0.65
    G.strokeWidth = 1.75
    G.textPadding = 7.0
    G.fontSize = "18pt"

    -- Needed to hand-tune the G.bgScaleFactor and G.sphere.y
    G.bgScaleFactor = 1.11

    G.labelPadding = -33.0
    G.label = "S^2"

    G.text = Text {
	x : G.circle.x + G.circle.r + G.labelPadding
	y : G.circle.y + G.circle.r + G.labelPadding
	string : G.label
	fontSize : G.fontSize
    }

    G.sphere = Image {
	 x : 0.0
	 y : -21.0 -- Hand-tuned
	 w : G.circle.r * 2.0 * G.bgScaleFactor
	 h : G.circle.r * 2.0 * G.bgScaleFactor
	 rotation: 0.0
	 path : "SphereBackground.svg"
    }

    G.labelLayering = G.text above G.sphere

    G.circle = Circle {
    		x : 0.0
		y : 0.0
		-- r : (G.sphereRadius * G.toScreen / G.dist_to_sphere_center) 
		r : 0.353 * G.toScreen
		-- The projective plane is at z=1 and the widest part of the sphere is at z=3
		-- Unintuitively, after projection, there is a point wider than the diameter of the sphere. That would project to z = +/- 1/3.
		-- But here is the widest z:
		-- circPts = map (\t -> (cos t, sin t)) [0, (2 * pi) / 100 .. 2 * pi] -- Unit circle in (x,z)
		-- maximum $ map (\(x, z) -> x/(z + 3)) circPts -- Move it forward then project on z=1
		-- 0.3534234568955392 -- This is wider than 1/3!!!
		-- color : setOpacity(Colors.blue, 0.1)
		color : Colors.none
		strokeWidth : 1.0
		-- strokeColor : Colors.red
		strokeColor : Colors.none
    }

    G.layering = G.circle below G.sphere
    -- For testing that the image is in the right location
    -- G.layering = G.circle above G.sphere
}

Point p {
      p.x0 = ?
      p.y0 = ?

      p.vec_math = calcZSphere(p.x0, p.y0, G.max_r)
      p.x = get(p.vec_math, 0)
      p.y = get(p.vec_math, 1)
      p.z = get(p.vec_math, 2)

      -- TODO: for visibility, only pick points on the front of the sphere
      -- These need to be inside the disk in order to satisfy the constraint.

      -- p.insetFn = ensure lessThanSq(normSq(p.x, p.y), G.max_r * G.max_r) -- Inset so it's not stuck at boundary

      -- p.insetFn = ensure lessThan(normSq(p.x, p.y), G.max_r * G.max_r) -- Inset so it's not stuck at boundary

      -- p.visibleFn = ensure lessThanSq(p.z0, G.max_z) -- `lessThan` doesn't seem to have any effect

      -- p.insetFn = ensure lessThanSq(calcNorm(p.x, p.y), G.max_r) -- Inset so it's not stuck at boundary

      -- x_screen, y_screen, z_projected (not converted to screen)
      p.vec_screen = projectAndToScreen(G.hfov, G.vfov, G.sphereRadius, G.camera, G.dir, p.vec_math, G.toScreen, p.shape.name)

      p.z_sphere_range = (-1.0 * G.sphereRadius + G.camera_z, G.sphereRadius + G.camera_z) -- Closer to camera, farther from camera
      p.z_screen_range = (4.0, 1.0) -- reverse lerp because it should be bigger if closer to camera

      -- TODO: open parser issue: no inline exprs here?
       p.shape = Circle {
         x : get(p.vec_screen, 0)
	 y : get(p.vec_screen, 1)
	       -- z in [-0.5,0.5] -> [1.5, 2.5] -> [3, 10]. 
	       -- TODO check/update this. This only works if z would lie on the sphere in math space
         -- r : max(scaleLinear(get(p.vec_screen, 2), p.z_sphere_range, p.z_screen_range), 0.0)
	 r : 3.0
	 color : Colors.black
	 strokeWidth : 1.0
	 strokeColor : Colors.black
       }

       p.text = Text {
	 -- x : p.shape.x + G.textMargin
	 -- y : p.shape.y + G.textMargin
	 x : ?
	 y : ?
	 fontSize : G.fontSize
	 string : p.label
	 rotation : 0.0
	 color : Colors.black
       }

       p.labelFnMargin = ensure atDist(p.shape, p.text, G.textPadding)       

       p.layeringSphere = p.shape above G.sphere
       p.layeringSphereText = p.text above G.sphere
}

Segment e
where e := MkSegment(p, q)
with Point p; Point q {
     -- Line on sphere. (Arc of a circle, or a geodesic)
     -- Interpolate between each one's sphere coords to yield several points
     -- then project all of them into screen space

     -- TODO: need to optimize or compute so that the *segments* also lie on the front of the sphere. How to do that?

     e.arcPath = slerp(p.vec_math, q.vec_math, G.numArcPts)
     e.screenspacePath = projectAndToScreen_list(G.hfov, G.vfov, G.sphereRadius, G.camera, G.dir, e.arcPath, G.toScreen, e.shape.name)

     -- Draw a path using the slerp'ed points
     e.shape = Curve {
     	     pathData : pathFromPoints(e.screenspacePath)
	     strokeWidth : G.strokeWidth
	     fill : Colors.none
	     color : Colors.black
	     rotation : 0.0
     }

     e.layering1 = p.shape above e.shape
     e.layering2 = q.shape above e.shape
     e.layeringSphere = e.shape above G.sphere

     LOCAL.labelAvoidFn_p = encourage repel(e.shape, p.text, G.repelWeight)
     LOCAL.labelAvoidFn_q = encourage repel(e.shape, q.text, G.repelWeight)
}

Ray r {
    r.layeringSphere = r.shape above G.sphere
}

Triangle t
where t := MkTriangle(p, q, r)
with Point p; Point q; Point r {
     t.arcPath_pq = slerp(p.vec_math, q.vec_math, G.numArcPts)
     t.screenspacePath_pq = projectAndToScreen_list(G.hfov, G.vfov, G.sphereRadius, G.camera, G.dir, t.arcPath_pq, G.toScreen, t.shape.name)

     t.arcPath_qr = slerp(q.vec_math, r.vec_math, G.numArcPts)
     t.screenspacePath_qr = projectAndToScreen_list(G.hfov, G.vfov, G.sphereRadius, G.camera, G.dir, t.arcPath_qr, G.toScreen, t.shape.name)

     t.arcPath_rp = slerp(r.vec_math, p.vec_math, G.numArcPts)
     t.screenspacePath_rp = projectAndToScreen_list(G.hfov, G.vfov, G.sphereRadius, G.camera, G.dir, t.arcPath_rp, G.toScreen, t.shape.name)

     t.shape = Curve {
     	     pathData : join(t.screenspacePath_pq, t.screenspacePath_qr, t.screenspacePath_rp) -- Connect them in a particular order
	     strokeWidth : 0.0
	     fill : setOpacity(Colors.darkpurple, 0.4)
	     color : Colors.none
	     rotation : 0.0
     }

     t.layeringSphere = t.shape above G.sphere
}

Ray r
with Segment s; Point p; Point q
where r := PerpendicularBisector(s); s := MkSegment(p, q) {

      r.rayArcLen = G.pi / 15.0
      r.tail_vec_math = halfwayPoint(p.vec_math, q.vec_math) -- The part w/o arrow
      r.head_vec_math = normalOnSphere(p.vec_math, q.vec_math, r.tail_vec_math, r.rayArcLen)
      r.arcPath = slerp(r.tail_vec_math, r.head_vec_math, G.numArcPts)
      r.screenspacePath = projectAndToScreen_list(G.hfov, G.vfov, G.sphereRadius, G.camera, G.dir, r.arcPath, G.toScreen, r.shape.name)

      -- Ray
      r.shape = Curve {
      	      pathData : pathFromPoints(r.screenspacePath)
	      strokeWidth : G.strokeWidth
	      fill : Colors.none
	      color : Colors.darkpurple
	      rotation : 0.0
	      rightArrowhead : True
	      arrowheadSize : G.arrowheadSize
      }

      r.perpArcLen = G.pi / 40.0
      r.perpmarkPath = perpPath(p.vec_math, q.vec_math, r.tail_vec_math, r.head_vec_math, r.perpArcLen)
      r.perpmark_screenspacePath = projectAndToScreen_list(G.hfov, G.vfov, G.sphereRadius, G.camera, G.dir, r.perpmarkPath, G.toScreen, r.shape.name)

      r.perpMark = Curve {
      	      pathData : polygonFromPoints(r.perpmark_screenspacePath)
	      strokeWidth : 1.25
	      color : Colors.black
	      fill : setOpacity(Colors.white, 0.5)
      }

      r.markLayering1 = r.perpMark below s.shape
      r.markLayering2 = r.perpMark below r.shape
      r.markLayering3 = r.perpMark above G.sphere
      r.markLayering4 = r.perpMark below p.shape
      r.markLayering5 = r.perpMark below q.shape
      r.markLayering6 = r.perpMark below r.shape
      r.markLayering7 = r.perpMark below m.shape

     LOCAL.labelAvoidFn_Ray = encourage repel(r.perpMark, m.text, G.repelWeight)
     LOCAL.labelAvoidFn_Seg = encourage repel(s.shape, m.text, G.repelWeight)
}

Point p
where p := Midpoint(s); s := MkSegment(a, b)
with Segment s; Point a; Point b {
     override p.vec_math = halfwayPoint(a.vec_math, b.vec_math)
     
     -- delete p.visibleFn

     override p.shape.color = Colors.white
     override p.shape.r = 3.2
     p.midLayering = p.shape above s.shape
}

-- Assuming the segments share a point
Angle theta
where theta := InteriorAngle(q, p, r)
with Point p; Point q; Point r {

     -- TODO: if the radius is larger, it becomes clear that the arc doesn't quite lie on the sphere. Fix this
     theta.radius = G.pi / 20.0
     theta.arcPath = arcPath(p.vec_math, q.vec_math, r.vec_math, theta.radius)
     theta.arcPath_screenspace = projectAndToScreen_list(G.hfov, G.vfov, G.sphereRadius, G.camera, G.dir, theta.arcPath, G.toScreen, theta.shape.name)

     theta.shape = Curve {
     		 pathData : polygonFromPoints(theta.arcPath_screenspace)
		 strokeWidth : G.strokeWidth
		 color : Colors.darkpurple
		 fill : setOpacity(Colors.white, 0.5)
     }

     theta.layeringSphere = theta.shape above G.sphere
     theta.layering2 = theta.shape below p.shape
     theta.layering3 = theta.shape below q.shape
     theta.layering4 = theta.shape below r.shape

     LOCAL.labelAvoidFn_Seg = encourage repel(theta.shape, p.text, G.repelWeight)
}

Ray r
with Angle theta; Point x; Point y; Point z
where r := Bisector(theta); theta := InteriorAngle(y, x, z) {

      -- TODO: note that there's a decent amount of code shared between here and the PerpendicularBisector selector
      -- Move some into the basic Ray selector?
      r.rayArcLen = G.pi / 6.0
      r.tail_vec_math = x.vec_math
      r.head_vec_math = angleBisector(x.vec_math, y.vec_math, z.vec_math, r.rayArcLen)
      r.arcPath = slerp(r.tail_vec_math, r.head_vec_math, G.numArcPts)
      r.screenspacePath = projectAndToScreen_list(G.hfov, G.vfov, G.sphereRadius, G.camera, G.dir, r.arcPath, G.toScreen, r.shape.name)

      r.shape = Curve {
      	      pathData : pathFromPoints(r.screenspacePath)
	      strokeWidth : G.strokeWidth
	      fill : Colors.none
	      color : Colors.darkpurple
	      rotation : 0.0
	      rightArrowhead : True
	      arrowheadSize : G.arrowheadSize
      }

      -- Bisect the arc twice more to get the bisector mark locations
      -- Throw away z coordinate for each
      theta.bisectpt1 = angleBisector(x.vec_math, y.vec_math, r.head_vec_math, theta.radius)
      theta.vec_screen1 = projectAndToScreen(G.hfov, G.vfov, G.sphereRadius, G.camera, G.dir, theta.bisectpt1, G.toScreen, theta.bisectMark1.name)
      theta.vec_pt1 = xy(theta.vec_screen1)

      theta.bisectpt2 = angleBisector(x.vec_math, z.vec_math, r.head_vec_math, theta.radius)
      theta.vec_screen2 = projectAndToScreen(G.hfov, G.vfov, G.sphereRadius, G.camera, G.dir, theta.bisectpt2, G.toScreen, theta.bisectMark2.name)
      theta.vec_pt2 = xy(theta.vec_screen2)

      theta.markLen = 10.0

      LOCAL.x_proj = xy(x.vec_screen)

      -- Angle bisector marks: two tick marks
      -- NOTE: seems to make things slower
      -- Also note: drawing as straight lines, not geodesics on sphere

      theta.bisectMark1 = Curve {
      	      pathData : makeBisectorMark(theta.vec_pt1, LOCAL.x_proj, theta.markLen)
      	      strokeWidth : G.strokeWidth
      	      fill : Colors.none
      	      color : Colors.darkpurple
      	      rotation : 0.0
      	      rightArrowhead : False
      	      arrowheadSize : 0.0
      }

      theta.bisectMark2 = Curve {
      	      pathData : makeBisectorMark(theta.vec_pt2, LOCAL.x_proj, theta.markLen)
      	      strokeWidth : G.strokeWidth
      	      fill : Colors.none
      	      color : Colors.darkpurple
      	      rotation : 0.0
      	      rightArrowhead : False
      	      arrowheadSize : 0.0
      }

      theta.layeringMark1 = theta.bisectMark1 above theta.shape
      theta.layeringMark2 = theta.bisectMark2 above theta.shape

     LOCAL.labelAvoidFn_Ray = encourage repel(r.shape, x.text, G.repelWeight)
}

Triangle t; Angle theta {
	 LOCAL.layering = theta.shape above t.shape
}

Triangle t; Ray r {
	 LOCAL.layering = r.shape above t.shape
}

Point p; Point q {
      LOCAL.shapeRepelFn = encourage repel(p.shape, q.shape, G.repelWeight2)
      LOCAL.labelRepelFn = encourage repel(p.text, q.text, G.repelWeight2)
}

Point p; Ray r {
     LOCAL.labelAvoidFn_Ray = encourage repel(r.shape, p.text, G.repelWeight2)
}

Point p; Segment s {
     LOCAL.labelAvoidFn_Ray = encourage repel(s.shape, p.text, G.repelWeight2)
}
